﻿using Irony.Parsing;

namespace Gumini.JulaDB.Parser
{
    // Loosely based on SQL89 grammar from Gold parser. Supports some extra TSQL constructs.

    [Language("SQL", "89", "SQL 89 grammar")]
    public class SqlGrammar : Grammar
    {
        //Non-terminals
        public readonly NonTerminal Id = new NonTerminal("Id");
        public readonly NonTerminal stmt = new NonTerminal("stmt");
        public readonly NonTerminal createTableStmt = new NonTerminal("createTableStmt");
        public readonly NonTerminal createIndexStmt = new NonTerminal("createIndexStmt");
        public readonly NonTerminal alterStmt = new NonTerminal("alterStmt");
        public readonly NonTerminal dropTableStmt = new NonTerminal("dropTableStmt");
        public readonly NonTerminal dropIndexStmt = new NonTerminal("dropIndexStmt");
        public readonly NonTerminal selectStmt = new NonTerminal("selectStmt");
        public readonly NonTerminal insertStmt = new NonTerminal("insertStmt");
        public readonly NonTerminal updateStmt = new NonTerminal("updateStmt");
        public readonly NonTerminal deleteStmt = new NonTerminal("deleteStmt");
        public readonly NonTerminal fieldDef = new NonTerminal("fieldDef");
        public readonly NonTerminal fieldDefList = new NonTerminal("fieldDefList");
        public readonly NonTerminal nullSpecOpt = new NonTerminal("nullSpecOpt");
        public readonly NonTerminal typeName = new NonTerminal("typeName");
        public readonly NonTerminal typeSpec = new NonTerminal("typeSpec");
        public readonly NonTerminal typeParamsOpt = new NonTerminal("typeParams");
        public readonly NonTerminal constraintDef = new NonTerminal("constraintDef");
        public readonly NonTerminal constraintListOpt = new NonTerminal("constraintListOpt");
        public readonly NonTerminal constraintTypeOpt = new NonTerminal("constraintTypeOpt");
        public readonly NonTerminal idList = new NonTerminal("idlist");
        public readonly NonTerminal idlistPar = new NonTerminal("idlistPar");
        public readonly NonTerminal aliasedId = new NonTerminal("aliasedId");
        public readonly NonTerminal uniqueOpt = new NonTerminal("uniqueOpt");
        public readonly NonTerminal orderList = new NonTerminal("orderList");
        public readonly NonTerminal orderMember = new NonTerminal("orderMember");
        public readonly NonTerminal orderDirOpt = new NonTerminal("orderDirOpt");
        public readonly NonTerminal withClauseOpt = new NonTerminal("withClauseOpt");
        public readonly NonTerminal alterCmd = new NonTerminal("alterCmd");
        public readonly NonTerminal insertData = new NonTerminal("insertData");
        public readonly NonTerminal intoOpt = new NonTerminal("intoOpt");
        public readonly NonTerminal assignList = new NonTerminal("assignList");
        public readonly NonTerminal whereClauseOpt = new NonTerminal("whereClauseOpt");
        public readonly NonTerminal assignment = new NonTerminal("assignment");
        public readonly NonTerminal expression = new NonTerminal("expression");
        public readonly NonTerminal exprList = new NonTerminal("exprList");
        public readonly NonTerminal selRestrOpt = new NonTerminal("selRestrOpt");
        public readonly NonTerminal selList = new NonTerminal("selList");
        public readonly NonTerminal intoClauseOpt = new NonTerminal("intoClauseOpt");
        public readonly NonTerminal fromClauseOpt = new NonTerminal("fromClauseOpt");
        public readonly NonTerminal groupClauseOpt = new NonTerminal("groupClauseOpt");
        public readonly NonTerminal havingClauseOpt = new NonTerminal("havingClauseOpt");
        public readonly NonTerminal orderClauseOpt = new NonTerminal("orderClauseOpt");
        public readonly NonTerminal columnItemList = new NonTerminal("columnItemList");
        public readonly NonTerminal columnItem = new NonTerminal("columnItem");
        public readonly NonTerminal asOpt = new NonTerminal("asOpt");
        public readonly NonTerminal aliasOpt = new NonTerminal("aliasOpt");
        public readonly NonTerminal tuple = new NonTerminal("tuple");
        public readonly NonTerminal joinKindOpt = new NonTerminal("joinKindOpt");
        public readonly NonTerminal term = new NonTerminal("term");
        public readonly NonTerminal unExpr = new NonTerminal("unExpr");
        public readonly NonTerminal unOp = new NonTerminal("unOp");
        public readonly NonTerminal binExpr = new NonTerminal("binExpr");
        public readonly NonTerminal binOp = new NonTerminal("binOp");
        public readonly NonTerminal isNullExpr = new NonTerminal("isNullExpr");
        public readonly NonTerminal betweenExpr = new NonTerminal("betweenExpr");
        public readonly NonTerminal inExpr = new NonTerminal("inExpr");
        public readonly NonTerminal parSelectStmt = new NonTerminal("parSelectStmt");
        public readonly NonTerminal notOpt = new NonTerminal("notOpt");
        public readonly NonTerminal funCall = new NonTerminal("funCall");
        public readonly NonTerminal stmtLine = new NonTerminal("stmtLine");
        public readonly NonTerminal semiOpt = new NonTerminal("semiOpt");
        public readonly NonTerminal stmtList = new NonTerminal("stmtList");
        public readonly NonTerminal funArgs = new NonTerminal("funArgs");
        public readonly NonTerminal inStmt = new NonTerminal("inStmt");
        public readonly IdentifierTerminal Id_simple;
        public readonly StringLiteral string_literal = new StringLiteral("string", "'", StringOptions.AllowsDoubledQuote);
        public readonly NumberLiteral number = new NumberLiteral("number");
        public readonly NonTerminal joinSequenceOpt = new NonTerminal("joinSequenceOpt");
        public readonly NonTerminal joinSpecifier = new NonTerminal("joinSpecifier");

        public readonly KeyTerm
            comma,
            dot,
            asterisk,
            CREATE,
            NULL,
            NOT,
            UNIQUE,
            WITH,
            TABLE,
            ALTER,
            ADD,
            COLUMN,
            DROP,
            CONSTRAINT,
            INDEX,
            ON,
            KEY,
            PRIMARY,
            INSERT,
            INTO,
            UPDATE,
            SET,
            VALUES,
            DELETE,
            SELECT,
            FROM,
            AS,
            COUNT,
            JOIN,
            BY,
            TRUE,
            FALSE;

        public SqlGrammar()
            : base(false)
        { //SQL is case insensitive
            
            //Terminals
            
            var comment = new CommentTerminal("comment", "/*", "*/");
            var lineComment = new CommentTerminal("line_comment", "--", "\n", "\r\n");
            NonGrammarTerminals.Add(comment);
            NonGrammarTerminals.Add(lineComment);
            
            Id_simple = TerminalFactory.CreateSqlExtIdentifier(this, "id_simple"); //covers normal identifiers (abc) and quoted id's ([abc d], "abc d")

            comma = ToTerm(",");
            dot = ToTerm(".");
            asterisk = ToTerm("*");
            CREATE = ToTerm("CREATE");
            NULL = ToTerm("NULL");
            NOT = ToTerm("NOT");
            UNIQUE = ToTerm("UNIQUE");
            WITH = ToTerm("WITH");
            TABLE = ToTerm("TABLE");
            ALTER = ToTerm("ALTER");
            ADD = ToTerm("ADD");
            COLUMN = ToTerm("COLUMN");
            DROP = ToTerm("DROP");
            CONSTRAINT = ToTerm("CONSTRAINT");
            INDEX = ToTerm("INDEX");
            ON = ToTerm("ON");
            KEY = ToTerm("KEY");
            PRIMARY = ToTerm("PRIMARY");
            INSERT = ToTerm("INSERT");
            INTO = ToTerm("INTO");
            UPDATE = ToTerm("UPDATE");
            SET = ToTerm("SET");
            VALUES = ToTerm("VALUES");
            DELETE = ToTerm("DELETE");
            SELECT = ToTerm("SELECT");
            FROM = ToTerm("FROM");
            AS = ToTerm("AS");
            COUNT = ToTerm("COUNT");
            JOIN = ToTerm("JOIN");
            BY = ToTerm("BY");
            TRUE = ToTerm("TRUE");
            FALSE = ToTerm("FALSE");

            //BNF Rules
            this.Root = stmtList;
            stmtLine.Rule = stmt + semiOpt;
            semiOpt.Rule = Empty | ";";
            stmtList.Rule = MakePlusRule(stmtList, stmtLine);

            //ID
            Id.Rule = MakePlusRule(Id, dot, Id_simple);

            stmt.Rule = createTableStmt | createIndexStmt | alterStmt
                      | dropTableStmt | dropIndexStmt
                      | selectStmt | insertStmt | updateStmt | deleteStmt
                      | "GO";
            //Create table
            createTableStmt.Rule = CREATE + TABLE + Id + "(" + fieldDefList + ")" + constraintListOpt;
            fieldDefList.Rule = MakePlusRule(fieldDefList, comma, fieldDef);
            fieldDef.Rule = Id + typeName + typeParamsOpt + nullSpecOpt;
            nullSpecOpt.Rule = NULL | NOT + NULL | Empty;
            typeName.Rule = ToTerm("BIT") | "DATE" | "TIME" | "TIMESTAMP" | "DECIMAL" | "REAL" | "FLOAT" | "SMALLINT" | "INTEGER"
                                         | "INTERVAL" | "CHARACTER"
                // MS SQL types:  
                                         | "DATETIME" | "INT" | "DOUBLE" | "CHAR" | "NCHAR" | "VARCHAR" | "NVARCHAR"
                                         | "IMAGE" | "TEXT" | "NTEXT";
            typeParamsOpt.Rule = "(" + number + ")" | "(" + number + comma + number + ")" | Empty;
            constraintDef.Rule = CONSTRAINT + Id + constraintTypeOpt;
            constraintListOpt.Rule = MakeStarRule(constraintListOpt, constraintDef);
            constraintTypeOpt.Rule = PRIMARY + KEY + idlistPar | UNIQUE + idlistPar | NOT + NULL + idlistPar
                                   | "Foreign" + KEY + idlistPar + "References" + Id + idlistPar;
            idlistPar.Rule = "(" + idList + ")";
            idList.Rule = MakePlusRule(idList, comma, Id);

            //Create Index
            createIndexStmt.Rule = CREATE + uniqueOpt + INDEX + Id + ON + Id + orderList + withClauseOpt;
            uniqueOpt.Rule = Empty | UNIQUE;
            orderList.Rule = MakePlusRule(orderList, comma, orderMember);
            orderMember.Rule = Id + orderDirOpt;
            orderDirOpt.Rule = Empty | "ASC" | "DESC";
            withClauseOpt.Rule = Empty | WITH + PRIMARY | WITH + "Disallow" + NULL | WITH + "Ignore" + NULL;

            //Alter 
            alterStmt.Rule = ALTER + TABLE + Id + alterCmd;
            alterCmd.Rule = ADD + COLUMN + fieldDefList + constraintListOpt
                          | ADD + constraintDef
                          | DROP + COLUMN + Id
                          | DROP + CONSTRAINT + Id;

            //Drop stmts
            dropTableStmt.Rule = DROP + TABLE + Id;
            dropIndexStmt.Rule = DROP + INDEX + Id + ON + Id;

            //Insert stmt
            insertStmt.Rule = INSERT + intoOpt + Id + idlistPar + insertData;
            insertData.Rule = selectStmt | VALUES + "(" + exprList + ")";
            intoOpt.Rule = Empty | INTO; //Into is optional in MSSQL

            //Update stmt
            updateStmt.Rule = UPDATE + Id + SET + assignList + whereClauseOpt;
            assignList.Rule = MakePlusRule(assignList, comma, assignment);
            assignment.Rule = Id + "=" + expression;

            //Delete stmt
            deleteStmt.Rule = DELETE + FROM + Id + whereClauseOpt;

            //Select stmt
            selectStmt.Rule = SELECT + selRestrOpt + selList + intoClauseOpt + fromClauseOpt + whereClauseOpt +
                              groupClauseOpt + havingClauseOpt + orderClauseOpt;
            selRestrOpt.Rule = Empty | "ALL" | "DISTINCT";
            selList.Rule = asterisk | columnItemList;
            columnItemList.Rule = MakePlusRule(columnItemList, comma, columnItem);
            columnItem.Rule = expression + aliasOpt;
            aliasOpt.Rule = Empty | asOpt + Id;
            asOpt.Rule = Empty | AS;
            intoClauseOpt.Rule = Empty | INTO + Id;
            fromClauseOpt.Rule = Empty | FROM + aliasedId + joinSequenceOpt;
            joinSequenceOpt.Rule = MakeStarRule(joinSequenceOpt, joinSpecifier);
            joinSpecifier.Rule = joinKindOpt + JOIN + aliasedId + ON + expression | comma + aliasedId;
            aliasedId.Rule = Id + aliasOpt;
            joinKindOpt.Rule = Empty | "INNER" | "LEFT" | "RIGHT";

            whereClauseOpt.Rule = Empty | "WHERE" + expression;
            groupClauseOpt.Rule = Empty | "GROUP" + BY + exprList;
            havingClauseOpt.Rule = Empty | "HAVING" + expression;
            orderClauseOpt.Rule = Empty | "ORDER" + BY + orderList;

            //Expression
            exprList.Rule = MakePlusRule(exprList, comma, expression);
            expression.Rule = term | unExpr | binExpr | asterisk | isNullExpr; // | betweenExpr; //-- BETWEEN doesn't work - yet; brings a few parsing conflicts 
            term.Rule = NULL | TRUE | FALSE | Id | string_literal | number | funCall | tuple | parSelectStmt;// | inStmt;
            tuple.Rule = "(" + exprList + ")";
            parSelectStmt.Rule = "(" + selectStmt + ")";
            unExpr.Rule = unOp + expression;
            unOp.Rule = NOT | "+" | "-" | "~";
            binExpr.Rule = expression + binOp + expression;
            binOp.Rule = ToTerm("+") | "-" | "*" | "/" | "%" // arithmetic
                       | "&" | "|" | "^"                     // bit
                       | "||"                                // string
                       | "=" | ">" | "<" | ">=" | "<=" | "<>" | "!=" | "!<" | "!>"
                       | "AND" | "OR" | "LIKE" | NOT + "LIKE" | "IN" | NOT + "IN";
            isNullExpr.Rule = expression + "IS" + notOpt + NULL;
            betweenExpr.Rule = expression + notOpt + "BETWEEN" + expression + "AND" + expression;
            notOpt.Rule = Empty | NOT;
            //funCall covers some psedo-operators and special forms like ANY(...), SOME(...), ALL(...), EXISTS(...), IN(...)
            funCall.Rule = Id + "(" + funArgs + ")";
            funArgs.Rule = selectStmt | exprList;
            inStmt.Rule = expression + "IN" + "(" + exprList + ")";

            //Operators
            RegisterOperators(10, "*", "/", "%");
            RegisterOperators(9, "+", "-", "||");
            RegisterOperators(8, "=", ">", "<", ">=", "<=", "<>", "!=", "!<", "!>", "LIKE", "IN");
            RegisterOperators(7, "^", "&", "|");
            RegisterOperators(6, NOT);
            RegisterOperators(5, "AND");
            RegisterOperators(4, "OR");

            MarkPunctuation(",", "(", ")");
            MarkPunctuation(asOpt, semiOpt);
            //Note: we cannot declare binOp as transient because it includes operators "NOT LIKE", "NOT IN" consisting of two tokens. 
            // Transient non-terminals cannot have more than one non-punctuation child nodes.
            // Instead, we set flag InheritPrecedence on binOp , so that it inherits precedence value from it's children, and this precedence is used
            // in conflict resolution when binOp node is sitting on the stack
            base.MarkTransient(stmt, term, asOpt, aliasOpt, stmtLine, expression, unOp, tuple);
            binOp.SetFlag(TermFlags.InheritPrecedence);

        }
    }
}
